//
//  ChatStreamResult.swift
//  
//
//  Created by Sergii Kryvoblotskyi on 15/05/2023.
//

import Foundation

/// Represents a streamed chunk of a chat completion response returned
/// by the model, based on the provided input.
/// [Learn more](https://platform.openai.com/docs/guides/streaming-responses).
public struct ChatStreamResult: Codable, Equatable, Sendable {
    public struct Choice: Codable, Equatable, Sendable {
        public typealias FinishReason = ChatResult.Choice.FinishReason

        public struct ChoiceDelta: Codable, Equatable, Sendable {
            public typealias Role = ChatQuery.ChatCompletionMessageParam.Role

            /// The contents of the chunk message.
            public let content: String?
            /// If the audio output modality is requested, this object contains data about the audio response from the model.
            public let audio: ChoiceDeltaAudio?

            /// The role of the author of this message.
            public let role: Self.Role?
            public let toolCalls: [Self.ChoiceDeltaToolCall]?

            /// Value for `reasoning` field in response.
            ///
            /// Provided by:
            /// - Gemini (in OpenAI compatibility mode)
            ///   https://github.com/MacPaw/OpenAI/issues/283#issuecomment-2711396735
            /// - OpenRouter
            internal let _reasoning: String?

            /// Value for `reasoning_content` field.
            ///
            /// Provided by:
            /// - Deepseek
            ///   https://api-docs.deepseek.com/api/create-chat-completion#responses
            internal let _reasoningContent: String?

            /// Reasoning content.
            ///
            /// Supported response fields:
            /// - `reasoning` (Gemini, OpenRouter)
            /// - `reasoning_content` (Deepseek)
            public var reasoning: String? {
                _reasoning ?? _reasoningContent
            }

            public struct ChoiceDeltaAudio: Codable, Equatable, Sendable {

                /// Unique identifier for this audio response.
                public let id: String?
                /// Transcript of the audio generated by the model.
                public let transcript: String?
                /// The Unix timestamp (in seconds) for when this audio response will no longer be accessible on the server for use in multi-turn conversations.
                public let expiresAt: Int?
                /// Base64 encoded audio bytes generated by the model, in the format specified in the request.
                public let data: String?

                public enum CodingKeys: String, CodingKey {
                    case id
                    case transcript
                    case data
                    case expiresAt = "expires_at"
                }
            }

            public struct ChoiceDeltaToolCall: Codable, Equatable, Sendable {

                public let index: Int
                /// The ID of the tool call.
                public let id: String?
                /// The function that the model called.
                public let function: Self.ChoiceDeltaToolCallFunction?
                /// The type of the tool. Currently, only function is supported.
                public let type: String?

                public init(
                    index: Int,
                    id: String? = nil,
                    function: Self.ChoiceDeltaToolCallFunction? = nil
                ) {
                    self.index = index
                    self.id = id
                    self.function = function
                    self.type = "function"
                }

                public struct ChoiceDeltaToolCallFunction: Codable, Equatable, Sendable {

                    /// The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function.
                    public let arguments: String?
                    /// The name of the function to call.
                    public let name: String?

                    public init(
                        arguments: String? = nil,
                        name: String? = nil
                    ) {
                        self.arguments = arguments
                        self.name = name
                    }
                }
            }

            public enum CodingKeys: String, CodingKey {
                case content
                case audio
                case role
                case toolCalls = "tool_calls"
                case _reasoning = "reasoning"
                case _reasoningContent = "reasoning_content"
            }
        }

        /// The index of the choice in the list of choices.
        public let index: Int
        /// A chat completion delta generated by streamed model responses.
        public let delta: Self.ChoiceDelta
        /// The reason the model stopped generating tokens.
        /// This will be `stop` if the model hit a natural stop point or a provided stop sequence, `length` if the maximum number of tokens specified in the request was reached, `content_filter` if content was omitted due to a flag from our content filters, `tool_calls` if the model called a tool, or `function_call` (deprecated) if the model called a function.
        public let finishReason: FinishReason?
        /// Log probability information for the choice.
        public let logprobs: Self.ChoiceLogprobs?

        public struct ChoiceLogprobs: Codable, Equatable, Sendable {
            /// A list of message content tokens with log probability information.
            public let content: [Self.ChatCompletionTokenLogprob]?

            public struct ChatCompletionTokenLogprob: Codable, Equatable, Sendable {
                /// The token.
                public let token: String
                /// A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be null if there is no bytes representation for the token.
                public let bytes: [Int]?
                /// The log probability of this token.
                public let logprob: Double
                /// List of the most likely tokens and their log probability, at this token position. In rare cases, there may be fewer than the number of requested top_logprobs returned.
                public let topLogprobs: [Self.TopLogprob]?

                public struct TopLogprob: Codable, Equatable, Sendable {
                    /// The token.
                    public let token: String
                    /// A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be null if there is no bytes representation for the token.
                    public let bytes: [Int]?
                    /// The log probability of this token.
                    public let logprob: Double
                }

                public enum CodingKeys: String, CodingKey {
                    case token
                    case bytes
                    case logprob
                    case topLogprobs = "top_logprobs"
                }
            }
        }

        public enum CodingKeys: String, CodingKey {
            case index
            case delta
            case finishReason = "finish_reason"
            case logprobs
        }
    }

    /// A unique identifier for the chat completion. Each chunk has the same ID.
    public let id: String

    /// The object type, which is always `chat.completion.chunk`.
    public let object: String

    /// The Unix timestamp (in seconds) of when the chat completion was created. Each chunk has the same timestamp.
    public let created: TimeInterval

    /// The model to generate the completion.
    public let model: String
    
    /// A list of chat completion choices. Can contain more than one element if `n` is greater than 1.
    /// Can also be empty for the last chunk if you set `stream_options: {"include_usage": true}`.
    public let choices: [Choice]
    
    /// This fingerprint represents the backend configuration that the model runs with.
    /// Can be used in conjunction with the `seed` request parameter to understand when backend changes
    /// have been made that might impact determinism.
    ///
    /// Note: Even though [API Reference - The chat completion chunk object - system_fingerprint](https://platform.openai.com/docs/api-reference/chat-streaming/streaming#chat-streaming/streaming-system_fingerprint) declares the type as `string` (aka non-optional) - the response chunk may not contain the value, so we have had to make it optional `String?` in the Swift type
    /// See https://github.com/MacPaw/OpenAI/issues/331 for more details on such a case.
    public let systemFingerprint: String?
    
    /// Usage statistics for the completion request.
    public let usage: ChatResult.CompletionUsage?
    
    /// Specifies the latency tier to use for processing the request. This parameter is relevant
    /// for customers subscribed to the scale tier service:
    ///
    /// - If set to 'auto', and the Project is Scale tier enabled, the system will utilize scale tier credits until they are exhausted.
    /// - If set to 'auto', and the Project is not Scale tier enabled, the request will be processed using the default service tier with a lower uptime SLA and no latency guarantee.
    /// - If set to 'default', the request will be processed using the default service tier with a lower uptime SLA and no latency guarantee.
    /// - If set to 'flex', the request will be processed with the Flex Processing service tier.
    ///   [Learn more](https://platform.openai.com/docs/guides/flex-processing).
    /// - When not set, the default behavior is 'auto'.
    ///
    /// When this parameter is set, the response body will include the `service_tier` utilized.
    public let serviceTier: ServiceTier?
    
    /// A list of citations for the completion.
    ///
    /// - Note: the field is not a part of OpenAI API but is used by other providers
    public let citations: [String]?

    public enum CodingKeys: String, CodingKey {
        case id
        case object
        case created
        case model
        case citations
        case choices
        case systemFingerprint = "system_fingerprint"
        case usage
        case serviceTier = "service_tier"
    }
    
    public init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let parsingOptions = decoder.userInfo[.parsingOptions] as? ParsingOptions ?? []
        
        self.id = try container.decodeString(forKey: .id, parsingOptions: parsingOptions)
        self.object = try container.decodeString(forKey: .object, parsingOptions: parsingOptions)
        self.created = try container.decode(TimeInterval.self, forKey: .created)
        self.model = try container.decodeString(forKey: .model, parsingOptions: parsingOptions)
        self.citations = try container.decodeIfPresent([String].self, forKey: .citations)
        self.choices = try container.decode([ChatStreamResult.Choice].self, forKey: .choices)
        self.systemFingerprint = try container.decodeIfPresent(String.self, forKey: .systemFingerprint)
        
        // Even though API Reference declares that usage field should be either informative or null: https://platform.openai.com/docs/api-reference/chat/create#chat-create-stream_options
        // In some cases it can be present, but empty: https://github.com/MacPaw/OpenAI/issues/338
        //
        // To make things simpler, we're not going to check the correctnes of payload before trying to decode
        // We're just going to ignore all the errors here by using optional try and fallback to nil `usage`
        self.usage = try? container.decodeIfPresent(ChatResult.CompletionUsage.self, forKey: .usage)
        self.serviceTier = try container.decodeIfPresent(ServiceTier.self, forKey: .serviceTier)
    }
}
